package com.tyros.common.secure.password;

import com.tyros.common.secure.CryptoException;
import com.tyros.common.secure.codec.Codec;
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
import org.bouncycastle.crypto.params.Argon2Parameters;
import org.bouncycastle.util.Arrays;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.security.SecureRandom;
import java.util.regex.Pattern;

/**
 * copy from spring security
 *
 * @author zsy
 */
@SuppressWarnings("ALL")
public class Argon2PasswordEncoder implements PasswordEncoder {
    private static final int DEFAULT_SALT_LENGTH = 16;
    private static final int DEFAULT_HASH_LENGTH = 32;
    private static final int DEFAULT_PARALLELISM = 1;
    private static final int DEFAULT_MEMORY = 4096;
    private static final int DEFAULT_ITERATIONS = 3;
    private final Logger logger;
    private final int hashLength;
    private final int parallelism;
    private final int memory;
    private final int iterations;
    private final int saltLength;
    private final SecureRandom secureRandom;

    private static final Pattern PATTERN = Pattern.compile("^\\$argon2[i]?[d]?\\$v=\\d+\\$m=\\d+,t=\\d+,p=\\d+\\$.+");

    /**
     * @param saltLength  盐值长度
     * @param hashLength  密文长度
     * @param parallelism 并行度
     * @param memory      内存占用
     * @param iterations  迭代次数
     */
    public Argon2PasswordEncoder(int saltLength, int hashLength, int parallelism, int memory, int iterations) {
        this.logger = LoggerFactory.getLogger(this.getClass());
        this.hashLength = hashLength;
        this.parallelism = parallelism;
        this.memory = memory;
        this.iterations = iterations;
        this.saltLength = saltLength;
        this.secureRandom = new SecureRandom();
    }

    public Argon2PasswordEncoder() {
        this(DEFAULT_SALT_LENGTH, DEFAULT_HASH_LENGTH, DEFAULT_PARALLELISM, DEFAULT_MEMORY, DEFAULT_ITERATIONS);
    }

    @Override
    public String encode(String rawPassword) {
        byte[] salt = new byte[saltLength];
        secureRandom.nextBytes(salt);
        return encode(rawPassword, salt);
    }

    @Override
    public String encode(String rawPassword, byte[] salt) {
        byte[] hash = new byte[this.hashLength];
        Argon2Parameters params = (new Argon2Parameters.Builder(Argon2Parameters.ARGON2_id)).withSalt(salt).withParallelism(this.parallelism).withMemoryAsKB(this.memory).withIterations(this.iterations).build();
        Argon2BytesGenerator generator = new Argon2BytesGenerator();
        generator.init(params);
        generator.generateBytes(rawPassword.toCharArray(), hash);
        return encode(hash, params);
    }

    @Override
    public boolean matches(String rawPassword, String encodedPassword) {
        if (encodedPassword == null) {
            this.logger.warn("Cipher text is null");
            return false;
        } else if (!PATTERN.matcher(encodedPassword).matches()) {
            this.logger.warn("Cipher text is invalid");
            return false;
        } else {
            Argon2Hash decoded;
            try {
                decoded = decode(encodedPassword);
            } catch (IllegalArgumentException var6) {
                this.logger.warn("Malformed password hash", var6);
                return false;
            }

            byte[] hashBytes = new byte[decoded.getHash().length];
            Argon2BytesGenerator generator = new Argon2BytesGenerator();
            generator.init(decoded.getParameters());
            generator.generateBytes(rawPassword.toString().toCharArray(), hashBytes);
            return constantTimeArrayEquals(decoded.getHash(), hashBytes);
        }
    }

    @Override
    public boolean upgradeEncoding(String encodedPassword) {
        if (encodedPassword != null && encodedPassword.length() != 0) {
            Argon2Parameters parameters = decode(encodedPassword).getParameters();
            return parameters.getMemory() < this.memory || parameters.getIterations() < this.iterations;
        } else {
            this.logger.warn("password hash is null");
            return false;
        }
    }

    @Override
    public String[] split(String cipherText) {
        if (!PATTERN.matcher(cipherText).matches()) {
            throw new CryptoException("Cipher text is invalid");
        }
        String[] arrays = new String[2];
        arrays[0] = cipherText.substring(0, cipherText.lastIndexOf("$"));
        arrays[1] = cipherText.substring(cipherText.lastIndexOf("$") + 1);
        return arrays;
    }

    @Override
    public String merge(String cipherHeader, String cipherPayload) {
        return cipherHeader + "$" + cipherPayload;
    }

    private static boolean constantTimeArrayEquals(byte[] expected, byte[] actual) {
        if (expected.length != actual.length) {
            return false;
        } else {
            int result = 0;

            for (int i = 0; i < expected.length; ++i) {
                result |= expected[i] ^ actual[i];
            }

            return result == 0;
        }
    }

    public static String encode(byte[] hash, Argon2Parameters parameters) throws IllegalArgumentException {
        StringBuilder stringBuilder = new StringBuilder();
        switch (parameters.getType()) {
            case 0:
                stringBuilder.append("$argon2d");
                break;
            case 1:
                stringBuilder.append("$argon2i");
                break;
            case 2:
                stringBuilder.append("$argon2id");
                break;
            default:
                throw new CryptoException("Invalid algorithm type: " + parameters.getType());
        }

        stringBuilder.append("$v=").append(parameters.getVersion()).append("$m=").append(parameters.getMemory()).append(",t=").append(parameters.getIterations()).append(",p=").append(parameters.getLanes());
        if (parameters.getSalt() != null) {
            stringBuilder.append("$").append(Codec.BASE64.encode(parameters.getSalt()));
        }

        stringBuilder.append("$").append(Codec.BASE64.encode(hash));
        return stringBuilder.toString();
    }

    public static Argon2Hash decode(String encodedHash) throws IllegalArgumentException {
        String[] parts = encodedHash.split("\\$");
        if (parts.length < 4) {
            throw new CryptoException("Invalid encoded Argon2-hash");
        } else {
            int currentPart = 1;
            String var4 = parts[currentPart];
            byte var5 = -1;
            switch (var4.hashCode()) {
                case -1595243016:
                    if ("argon2id".equals(var4)) {
                        var5 = 2;
                    }
                    break;
                case -744196121:
                    if ("argon2d".equals(var4)) {
                        var5 = 0;
                    }
                    break;
                case -744196116:
                    if ("argon2i".equals(var4)) {
                        var5 = 1;
                    }
            }

            Argon2Parameters.Builder paramsBuilder;
            switch (var5) {
                case 0:
                    paramsBuilder = new Argon2Parameters.Builder(0);
                    break;
                case 1:
                    paramsBuilder = new Argon2Parameters.Builder(1);
                    break;
                case 2:
                    paramsBuilder = new Argon2Parameters.Builder(2);
                    break;
                default:
                    throw new CryptoException("Invalid algorithm type: " + parts[0]);
            }
            currentPart++;
            if (parts[currentPart].startsWith("v=")) {
                paramsBuilder.withVersion(Integer.parseInt(parts[currentPart].substring(2)));
                ++currentPart;
            }

            String[] performanceParams = parts[currentPart++].split(",");
            if (performanceParams.length != 3) {
                throw new CryptoException("Amount of performance parameters invalid");
            } else if (performanceParams[0].startsWith("m=")) {
                paramsBuilder.withMemoryAsKB(Integer.parseInt(performanceParams[0].substring(2)));
                if (performanceParams[1].startsWith("t=")) {
                    paramsBuilder.withIterations(Integer.parseInt(performanceParams[1].substring(2)));
                    if (performanceParams[2].startsWith("p=")) {
                        paramsBuilder.withParallelism(Integer.parseInt(performanceParams[2].substring(2)));
                        paramsBuilder.withSalt(Codec.BASE64.decode(parts[currentPart++]));
                        return new Argon2Hash(Codec.BASE64.decode(parts[currentPart]), paramsBuilder.build());
                    } else {
                        throw new CryptoException("Invalid parallelity parameter");
                    }
                } else {
                    throw new CryptoException("Invalid iterations parameter");
                }
            } else {
                throw new CryptoException("Invalid memory parameter");
            }
        }
    }

    public static class Argon2Hash {
        private byte[] hash;
        private Argon2Parameters parameters;

        Argon2Hash(byte[] hash, Argon2Parameters parameters) {
            this.hash = Arrays.clone(hash);
            this.parameters = parameters;
        }

        public byte[] getHash() {
            return Arrays.clone(this.hash);
        }

        public void setHash(byte[] hash) {
            this.hash = Arrays.clone(hash);
        }

        public Argon2Parameters getParameters() {
            return this.parameters;
        }

        public void setParameters(Argon2Parameters parameters) {
            this.parameters = parameters;
        }
    }
}

